package com.yby6.framework.web.service;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.yby6.common.config.YangBuYiConfig;
import com.yby6.common.constant.CacheConstants;
import com.yby6.common.constant.Constants;
import com.yby6.common.core.domain.ActiveUser;
import com.yby6.common.core.domain.SysUser;
import com.yby6.common.core.redis.RedisCache;
import com.yby6.common.core.redis.RedisUtils;
import com.yby6.common.enums.LoginType;
import com.yby6.common.enums.StateEnums;
import com.yby6.common.enums.UserStatus;
import com.yby6.common.exception.CustomException;
import com.yby6.common.exception.user.CaptchaException;
import com.yby6.common.exception.user.UserException;
import com.yby6.common.satoken.LoginHelper;
import com.yby6.common.spring.SpringUtils;
import com.yby6.common.utils.*;
import com.yby6.common.utils.ip.AddressUtils;
import com.yby6.common.utils.ip.IpUtils;
import com.yby6.system.domain.SysLog;
import com.yby6.system.service.SysLogService;
import com.yby6.system.service.SysMenuService;
import com.yby6.system.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.time.Duration;
import java.util.function.Supplier;

/**
 * @program: yangbuyi-rbac
 * @ClassName: SysLoginService
 * @create: 2021-05-13 17:09
 * @author: Yang Shuai
 * @since： JDK1.8
 * @SysLoginService: $登陆业务层
 **/
@Component
@RequiredArgsConstructor
public class SysLoginService {
    private static final Logger log = LoggerFactory.getLogger(SysLoginService.class);

    private final SysUserService sysUserService;

    private final SysMenuService sysMenuService;

    private final CaptchaService captchaService;

    private final RedisCache redisCache;

    @Value("${user.password.maxRetryCount}")
    private Integer maxRetryCount;

    @Value("${user.password.lockTime}")
    private Integer lockTime;


    /**
     * 单独处理登陆业务
     */
    public Serializable login(SysUser sysUser, HttpServletRequest request) {
        log.info("登录信息:{}", sysUser);
        SysUser user = loadUserByUsername(sysUser.getUsername());
        // 如果是系统注册的就校验密码 否则 第三方登录的就不校验了 旧数据的密码是错误的 我懒得改了
        if (ObjectUtil.isNotNull(user.getType()) && user.getType() == Constants.zero) {
            log.info("执行登录前校验");
            // 开始校验滑块验证码
            checkCode(sysUser);
            boolean login = !BCrypt.checkpw(SM2Util.decrypt(sysUser.getPassword()), user.getPassword());
            checkLogin(LoginType.PASSWORD, sysUser, () -> login);
        }
        // 此处可根据登录用户的数据不同 自行创建 loginUser
        ActiveUser loginUser = buildLoginUser(user);
        // 生成token
        LoginHelper.login(loginUser);
        saveLoginLog(sysUser, true, MessageUtils.message("user.login.success"), request);
        sysUser.setLoginTime(DateUtils.parseDate());
        sysUserService.updateByLoginTime(sysUser);
        return StpUtil.getTokenValue();
    }

    /**
     * 构建获取登录数据
     */
    private ActiveUser buildLoginUser(SysUser user) {
        ActiveUser loginUser = new ActiveUser();
        loginUser.setSysUser(user);
        loginUser.setRoles(sysUserService.selectRolesByUserId(user.getId()));
        loginUser.setPermissions(sysMenuService.selectMenuPermsByUserId(user.getId()));
        return loginUser;
    }

    /**
     * 加载用户信息
     *
     * @param username
     * @return
     */
    private SysUser loadUserByUsername(String username) {
        SysUser user = sysUserService.lambdaQuery().eq(SysUser::getUsername, username).one();
        if (ObjectUtil.isNull(user)) {
            log.info("登录用户：{} 不存在.", username);
            throw new UserException("user.not.exists", username);
        } else if (UserStatus.DELETED.getCode().equals(user.getDeleted())) {
            log.info("登录用户：{} 已被删除.", username);
            throw new UserException("user.password.delete", username);
        } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            log.info("登录用户：{} 已被停用.", username);
            throw new UserException("user.blocked", username);
        }
        return user;
    }

    /**
     * 校验验证码
     *
     * @param sysUser
     */
    private void checkCode(SysUser sysUser) {
        // 验证码类型
        String captchaType = sysUser.getParams().get("captchaType").toString();
        if (CharSequenceUtil.isBlank((String) sysUser.getParams().get("code"))) {
            throw new CustomException("验证码不能为空");
        }
        HttpServletRequest request = ServletUtils.getRequest();
        // 验证码类型使用
        if (captchaType.equals(Constants.CAPTCHA_TYPE_SLIDER)) {
            if (YangBuYiConfig.isTencentCaptcha()) {
                // 腾讯滑块开始
                // 获取前端回调函数返回的随机字符串
                String randStr = (String) sysUser.getParams().get("randStr");
                String token = (String) sysUser.getParams().get("token");
                boolean check = TencentCaptchaUtils.tencentCaptcha(randStr, token, request);
                // 校验
                if (!check) {
                    log.error("记录腾讯云滑块验证码错误:{}", request);
                    // 记录用户登录日志
                    saveLoginLog(sysUser, false, "腾讯云滑块验证码错误", request);
                    throw new CaptchaException();
                }
            } else {
                // 行为验证码安吉吉开始
                // 组装code实体
                CaptchaVO captchaVO = new CaptchaVO();
                captchaVO.setCaptchaVerification((String) sysUser.getParams().get("code"));
                // 开始校验
                ResponseModel response = captchaService.verification(captchaVO);
                if (!response.isSuccess()) {
                    log.error("行为验证码安吉吉滑块验证码错误:{}", request);
                    saveLoginLog(sysUser, false, "行为验证码安吉吉滑块验证码错误", request);
                    throw new CaptchaException();
                }
            }
        } else if (captchaType.equals(Constants.CAPTCHA_TYPE_GRAPH)) {
            // 验证码
            String code = sysUser.getParams().get(Constants.CODE).toString();
            // 验证码key
            String randomStr = sysUser.getParams().get(Constants.RANDOM_STR).toString();

            String key = Constants.CAPTCHA_CODE_KEY + randomStr;
            Object cacheObject = RedisUtils.getCacheObject(key);
            redisCache.deleteObject(key);
            if (ObjectUtil.isEmpty(cacheObject) || !code.equals(cacheObject)) {
                log.error("图形验证码错误:{}", request);
                saveLoginLog(sysUser, false, "图形验证码错误", request);
                throw new CaptchaException();
            }
        } else {
            // 验证码关闭
        }

    }

    /**
     * 登录校验
     */
    private void checkLogin(LoginType loginType, SysUser sysUser, Supplier<Boolean> supplier) {
        HttpServletRequest request = ServletUtils.getRequest();
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + sysUser.getUsername();
        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
        Integer errorNumber = RedisUtils.getCacheObject(errorKey);
        // 锁定时间内登录 则踢出
        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
            saveLoginLog(sysUser, false, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime), request);
            throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
        }

        if (supplier.get()) {
            // 是否第一次
            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
            // 达到规定错误次数 则锁定登录
            if (errorNumber.equals(maxRetryCount)) {
                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
                saveLoginLog(sysUser, false, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime), request);
                throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
            } else {
                // 未达到规定错误次数 则递增
                RedisUtils.setCacheObject(errorKey, errorNumber);
                saveLoginLog(sysUser, false, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime), request);
                throw new UserException(loginType.getRetryLimitCount(), errorNumber);
            }
        }

        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }

    /**
     * 保存登录日志
     *
     * @param sysUser          登录实体
     * @param isTrue           是否成功
     * @param localizedMessage not true remake
     * @param request
     */
    private void saveLoginLog(SysUser sysUser, boolean isTrue, String localizedMessage, HttpServletRequest request) {
        SysLog sysLog = new SysLog();
        sysLog.setLogParams(JSONUtil.toJsonStr(sysUser));
        // 记录日志
        sysLog.setLogUrl(request.getRequestURI());
        sysLog.setLogStatus(isTrue ? StateEnums.REQUEST_SUCCESS.getCode() : StateEnums.REQUEST_ERROR.getCode());
        sysLog.setLogMethod(request.getMethod());
        sysLog.setLogIp(IpUtils.getIpAddr(request));
        sysLog.setLogIpAddress(AddressUtils.getRealAddressByIP(IpUtils.getIpAddr(request)));
        sysLog.setLogType(1);
        sysLog.setCreatedBy(sysUser.getUsername());
        sysLog.setLogUa(request.getHeader("user-Agent"));
        sysLog.setLogController("login");
        sysLog.setLogTime(0L);
        sysLog.setLogResult(JsonUtils.toJsonString(localizedMessage));
        sysLog.setLogMessage(JsonUtils.toJsonString(localizedMessage));
        sysLog.setLogType(request.getRequestURI().contains(Constants.LOGIN_PATH) ? 1 : 2);
        // 调用日志
        SpringUtils.getBean(SysLogService.class).insert(sysLog);
    }

    /**
     * 退出登录
     */
    public void logout() {
        try {
            ActiveUser activeUser = LoginHelper.getSysUser();
            StpUtil.logout();
            saveLoginLog(activeUser.getSysUser(), true, MessageUtils.message("user.logout.success"), ServletUtils.getRequest());
        } catch (NotLoginException ignored) {
        }
    }
}
